#!/usr/bin/env python


import argparse
import ast
import json
import os
import re
import sys
from collections import defaultdict
from dataclasses import dataclass, field, fields
from typing import Dict, List, Union

import jsonpatch  # type: ignore
import jsonpointer  # type: ignore

# Python code generator to create new troposphere classes from the
# AWS resource specification.
#
# This generator works by reading in an AWS resource specification json file.
# The resources and properties are split apart to align with a given output
# file. In other words, a type such as AWS::Batch::JobDefinition will be
# put into the batch.py file.
#
# Since there are usually discrepancies in the docs or spec files plus the
# need for validation routines to be included there is additional processing
# done to the Resource Specification file and validators substituted into
# the code.
#
# For changes to the Resource Specification, there are jsonpatch files
# (located in scripts/patches) to fixup the json prior to emitting the code.
# A typical usage of this patch is when there is both a Resource and Property
# with the same name which cannot be emitted with the same class name. The
# jsonpatch file will usually rename the Property and then fixup any Resources
# using that Property.
#
# The validators are located in troposphere/validators with the common
# validators in __init__.py. This code generator will look for a corresponding
# file in this directory to locate validation functions. By parsing and walking
# the Python AST for this file it will extract function names and, based on a
# function docstring, will apply it as either a Property or Class validation
# function.
#
# Care is given to the output file to ensure pycodestyle and pyflakes tests
# will still pass. This incudes import declarations, class output ordering,
# and spacing considerations.
#
# Todo:
# - Currently only handles the single files (not the all-in-one)
#   (Note: but will deal with things like spec/GuardDuty*)
# - Handle adding in validators
# - Verify propery dependency/ordering in the file
# - Needs better error checking
# - Need to figure out the correct Timestamp type


stub = False
verbose = False

copyright_header = """\
# Copyright (c) 2012-2025, Mark Peek <mark@peek.org>
# All rights reserved.
#
# See LICENSE file for full license.
#
# *** Do not modify - this file is autogenerated ***

"""
spec_version = ""


def service_to_filename(service_name):
    service_map = {
        "kinesisanalytics": "analytics",
        "kinesisfirehose": "firehose",
        "lambda": "awslambda",
    }

    if service_name in service_map:
        return service_map[service_name]
    return service_name


class ResourceSpecDuplicateError(Exception):
    def __init__(self, message, names):
        self.message = message
        self.names = names


def to_dataclass(cls, d):
    flds = fields(cls)
    m = {f.metadata.get("mapname", f.name): f for f in flds}
    return cls(**{m[f].name: d[f] for f in d})


@dataclass
class Property:
    documentation: str = field(default="", metadata={"mapname": "Documentation"})
    duplicates_allowed: bool = field(
        default=False, metadata={"mapname": "DuplicatesAllowed"}
    )
    item_type: str = field(default="", metadata={"mapname": "ItemType"})
    primitive_item_type: str = field(
        default="", metadata={"mapname": "PrimitiveItemType"}
    )
    primitive_type: str = field(default="", metadata={"mapname": "PrimitiveType"})
    required: bool = field(default=False, metadata={"mapname": "Required"})
    type: str = field(default="", metadata={"mapname": "Type"})
    update_type: str = field(default="", metadata={"mapname": "UpdateType"})


@dataclass
class ResourceType:
    documentation: str
    name: str
    properties: Dict[str, Property]
    resource_name: str


@dataclass
class PropertyType:
    documentation: str
    properties: Dict[str, Property]
    resource_name: str


PropertyTypeDict = Dict[str, PropertyType]


class Service:
    def __init__(self):
        self.documentation: str = None
        self.resources: Dict[str, ResourceType] = {}
        self.properties: Dict[str, PropertyType] = {}
        self.standalone_types: Dict[str, Property] = {}
        self.class_validators = {}
        self.property_validators = defaultdict(dict)
        self.constants = []

    def get_property_items(self):
        """
        Generator to interate over all the resource and propery items
        """
        for class_name, resource_type in self.resources.items():
            for key, resource_value in resource_type.properties.items():
                yield (class_name, key, resource_value)
        for class_name, property_type in self.properties.items():
            for key, property_value in property_type.properties.items():
                yield (class_name, key, property_value)


class ResourceSpec:
    def __init__(self, filename: str):
        f = open(filename)
        self.spec = json.load(f)
        self.services: Dict[str, Service] = defaultdict(Service)

    def _patch(self):
        """
        Patch the json Resource Specification file to correct any issues that
        would prevent code generation. This uses jsonpatch with the patches
        coming in via python files (which allows for comments).
        """

        import importlib.util

        patch_dir = "scripts/patches"
        for patch_file in os.listdir(patch_dir):
            if patch_file.endswith(".py"):
                if patch_file == "__init__.py":
                    continue
                path = os.path.join(patch_dir, patch_file)
                import_spec = importlib.util.spec_from_file_location(
                    patch_file[:-3], path
                )
                patch = importlib.util.module_from_spec(import_spec)
                import_spec.loader.exec_module(patch)
                for p in patch.patches:
                    try:
                        self.spec = jsonpatch.apply_patch(self.spec, [p], in_place=True)
                    except jsonpointer.JsonPointerException:
                        print(f"jsonpatch error: {p}", file=sys.stderr)
                        raise
                    except jsonpatch.JsonPatchConflict:
                        print(f"jsonpatch error: {p}", file=sys.stderr)
                        path = p["path"]
                        print(f"path: {path}", file=sys.stderr)
                        raise

    def parse(self, limit_warnings=None):
        """
        Parse the json Resource Specification file into Python data structures.
        """

        global spec_version

        def get_class_name(name: str):
            return name.split(":")[4]

        def get_service_name(name: str):
            return name.split(":")[2].lower()

        def get_resource_name(name: str):
            """
            Short term backward compatibility hack for resources that need
            to be renamed. Put back together the real resource name such that:
                AWS::SNS::SubscriptionResource::Subscription
            will produce a class of "SubscriptionResource" and a resource name
            of "AWS::SNS::Subscription".
            """
            if name.count(":") == 6:
                name_list = name.split(":")
                return f"{name_list[0]}::{name_list[2]}::{name_list[6]}"
            return name

        self._patch()

        spec_version = self.spec["ResourceSpecificationVersion"]

        for resource_name, resource_dict in sorted(self.spec["ResourceTypes"].items()):
            service_name = get_service_name(resource_name)
            class_name = get_class_name(resource_name)
            service = self.services[service_name]
            documentation = resource_dict["Documentation"]

            properties = {}
            for k, v in sorted(resource_dict["Properties"].items()):
                properties[k] = to_dataclass(Property, v)
            service.resources[class_name] = ResourceType(
                documentation, class_name, properties, get_resource_name(resource_name)
            )

        for property_name, property_dict in sorted(self.spec["PropertyTypes"].items()):
            if property_name == "Tag":
                continue

            service_name = get_service_name(property_name)
            service = self.services[service_name]
            class_name = property_name.split(".")[1]

            documentation = None
            if "Documentation" in property_dict:
                documentation = property_dict["Documentation"]

            if "Properties" in property_dict:
                properties = {}
                for k, v in property_dict["Properties"].items():
                    properties[k] = to_dataclass(Property, v)

                if class_name in service.properties:
                    existing = service.properties[class_name]

                    existing_keys = list(sorted(existing.properties.keys()))
                    new_keys = list(sorted(properties.keys()))
                    if existing_keys != new_keys and (
                        limit_warnings is None or service_name in limit_warnings
                    ):
                        print(
                            f"Potential property conflict: {service_name} {class_name}",
                            file=sys.stderr,
                        )
                        print(
                            f"    {existing.resource_name}: {existing_keys}",
                            file=sys.stderr,
                        )
                        print(f"    {property_name}: {new_keys}", file=sys.stderr)
                        print("", file=sys.stderr)

                service.properties[class_name] = PropertyType(
                    documentation, properties, property_name
                )
            else:
                # No Properties, record as a standalone type
                service.standalone_types[class_name] = property_dict

        for service_name, service in self.services.items():
            self._get_validators(service_name, service)

        # Run some automatic "fixups" across the services
        self._fix_tags()
        self._fix_duplicate_names()
        self._fix_standalone_types()

        return self

    def _fix_tags(self):
        """
        Remap any "List of Tag" to Tags
        """

        SUPPORTED_TAG_NAMES = ["Tag", "Tags", "TagFormat", "TagsEntry"]
        UNSUPPORTED_TAG_NAMES = ["TagMap"]

        for service_name, service in self.services.items():
            for class_name, key, value in [x for x in service.get_property_items()]:
                if key in service.property_validators:
                    continue
                if value.item_type in UNSUPPORTED_TAG_NAMES:
                    print(
                        f"Unsupported Tag format found: {service_name} {class_name} {value.item_type}",
                        file=sys.stderr,
                    )
                    # Check we haven't deleted this already
                    if value.item_type in service.properties:
                        del service.properties[value.item_type]
                    value.type = None
                    value.primitive_item_type == "Json"
                elif value.type == "List" and value.item_type in SUPPORTED_TAG_NAMES:
                    value.type = "Tags"
                    # Check we haven't deleted this already
                    if value.item_type in service.properties:
                        del service.properties[value.item_type]
                elif value.type == "List" and value.primitive_item_type == "Json":
                    value.type = "Tags"

    def _fix_duplicate_names(self):
        """
        Find and fix duplication of Resource name and Property name by
        adding "Property" to the end of the Property name
        """

        def update_property(old, new: str, value: Property):
            if value.primitive_type == old:
                value.primitive_type = new
            elif value.type == old:
                value.type = new
            elif value.item_type == old:
                value.item_type = new

        for service_name, service in self.services.items():
            dups = []
            p = service.properties.keys()
            for class_name in sorted(service.resources.keys()):
                if class_name in p:
                    dups.append(class_name)

            if dups and verbose:
                print(f"Found dups in {service_name}: {dups}", file=sys.stderr)

            for dup_class_name in dups:
                new_class_name = f"{dup_class_name}Property"
                service.properties[new_class_name] = service.properties.pop(
                    dup_class_name
                )

                for class_name, key, value in service.get_property_items():
                    update_property(dup_class_name, new_class_name, value)

    def _fix_standalone_types(self):
        for service_name, service in self.services.items():
            for class_name, key, value in service.get_property_items():
                if key == "Tags":
                    continue

                if (
                    value.type in service.standalone_types
                    or value.item_type in service.standalone_types
                ):
                    # Determine if matching a type or item_type
                    if value.type in service.standalone_types:
                        update_name = value.type
                    else:
                        update_name = value.item_type

                    sat = service.standalone_types[update_name]

                    # Determine if updating a Resource or a Property
                    if class_name in service.properties:
                        props = service.properties[class_name].properties
                    else:
                        props = service.resources[class_name].properties

                    # Determine if the replacement it a Type or PrimitiveType
                    primitive = False
                    if "Type" in sat:
                        replacement_type = sat["Type"]
                    else:
                        replacement_type = sat["PrimitiveType"]
                        primitive = True

                    if props[key].type == update_name:
                        if primitive:
                            props[key].primitive_type = replacement_type
                        else:
                            props[key].type = replacement_type
                    else:
                        if primitive:
                            props[key].primitive_item_type = replacement_type
                            props[key].item_type = ""
                        else:
                            props[key].item_type = replacement_type

    def _get_validators(self, service_name, service):
        """
        Look for a validator file with a corresponding service name to
        import constants and validators. The validators are either attached
        to a property (Property: class.property_key) or a class (Class: class)
        by scanning the AST and parsing these values out of the docstring.
        """
        try:
            filename_base = service_to_filename(service_name)
            validator_filename = f"troposphere/validators/{filename_base}.py"
            file_contents = open(validator_filename).read()
        except FileNotFoundError:
            return

        # Look for these patterns to match where to apply validation functions
        class_re = re.compile(r"^Class: ([\w]*)", re.MULTILINE)
        export_re = re.compile(r"^Export:", re.MULTILINE)
        property_re = re.compile(r"^Property: ([\w]*)\.([\w]*)", re.MULTILINE)

        # Parse and walk the top-level AST of the validation code file
        tree = ast.parse(file_contents)
        for node in tree.body:
            if isinstance(node, ast.Assign):
                for t in node.targets:
                    service.constants.append(t.id)
            if isinstance(node, ast.AnnAssign):
                service.constants.append(node.target.id)
            if isinstance(node, ast.ClassDef) or isinstance(node, ast.FunctionDef):
                docstring = ast.get_docstring(node, clean=True)
                if docstring:
                    r = export_re.search(docstring)
                    if r:
                        service.constants.append(node.name)
            if isinstance(node, ast.FunctionDef):
                docstring = ast.get_docstring(node, clean=True)
                if docstring:
                    # Look for class level validation routines
                    r = class_re.search(docstring)
                    if r:
                        service.class_validators[r.group(1)] = node.name

                    # Look for property level validation routines
                    # r = property_re.search(docstring)
                    for m in property_re.finditer(docstring):
                        name = m.group(1)
                        property = m.group(2)
                        service.property_validators[name][property] = node.name


class Node:
    """Node object for building a per-file/service dependecy tree.

    Simple node object for creating and traversing the resource and
    property dependencies to emit code resources in a well-defined order.
    """

    def __init__(
        self,
        name: str,
        property_type: Union[PropertyType, ResourceType],
        documentation: str,
        resource_name: str,
    ):
        self.name = name
        self.property_type = property_type
        self.documentation = documentation
        self.resource_name = resource_name
        self.children: List[Node] = []

    def add_child(self, node):
        self.children.append(node)


class CodeGenerator:
    def __init__(self, service_name: str, service: Service):
        self.service_name = service_name
        self.service = service
        self.resources: Dict[str, ResourceType] = service.resources
        self.properties: Dict[str, PropertyType] = service.properties
        self.standalone_types: Dict[str, PropertyType] = service.standalone_types
        self.property_validators = service.property_validators
        self.statement_found = False

    def generate(self, file=None) -> str:
        """Generated the troposphere source code."""

        code = []

        # Output the copyright header
        code.append(copyright_header)

        # Output imports for commonly used classes
        if self.resources:
            code.append("from . import AWSObject")
        if self.properties:
            code.append("from . import AWSProperty")
        if self._walk_for_tags():
            code.append("from . import Tags")
        code.append("from . import PropsDictType")

        if not stub:
            # Output imports for commonly used validators
            if self._walk_for_type("Boolean"):
                code.append("from .validators import boolean")
            if self._walk_for_type("Integer") or self._walk_for_type("Long"):
                code.append("from .validators import integer")
            if self._walk_for_type("Double"):
                code.append("from .validators import double")

        # Output any constants defined in the validation code
        for v in self.service.constants:
            filename = service_to_filename(self.service_name)
            code.append(f"from .validators.{filename} import {v}  # noqa: F401")

        if not stub:
            # Output imports for any property validators found.
            property_imports = set()
            for k, d in self.service.property_validators.items():
                for validator in d.values():
                    property_imports.add(validator)
            for v in sorted(property_imports):
                filename = service_to_filename(self.service_name)
                code.append(f"from .validators.{filename} import {v}")

            # Output imports for any class validators found
            for k, v in self.service.class_validators.items():
                filename = service_to_filename(self.service_name)
                code.append(f"from .validators.{filename} import {v}")

        if stub and self._walk_for_stub_type("List"):
            code.append("from typing import List")

        # Now start outputting the classes
        seen: Dict[str, bool] = {}
        for class_name, resource_type in sorted(self.resources.items()):
            t = self._build_tree(
                class_name,
                resource_type,
                resource_type.documentation,
                resource_type.resource_name,
            )
            code += self._generate_tree(t, seen)

        # Look for any properties that were not emitted and include at the end, ignoring Tags
        property_set = set(self.properties.keys())
        seen_set = set(seen.keys())
        unseen = sorted(property_set.difference(seen_set))
        if unseen and verbose:
            print(
                f"unseen properties in service {self.service_name}: {unseen}",
                file=sys.stderr,
            )

        # Now let's look for dependencies between properties to output in
        # the correct order
        from functools import cmp_to_key

        def cmp(a, b):
            if a[0] in b[1]:
                return -1
            if b[0] in a[1]:
                return 1
            if a == b:
                return 0
            elif a < b:
                return -1
            else:
                return 1

        unseen_tuple = [(x, self._get_type_list(self.properties[x])) for x in unseen]
        unseen = [x[0] for x in sorted(unseen_tuple, key=cmp_to_key(cmp))]

        for property_name in unseen:
            n = Node(property_name, self.properties[property_name], "", "")
            code += self._generate_tree(n, seen)

        return "\n".join(code)

    def _build_tree(
        self,
        name: str,
        property_type: Union[PropertyType, ResourceType],
        documentation: str,
        resource_name=None,
    ) -> Node:
        """Build a tree of non-primitive typed dependency order."""
        n = Node(name, property_type, documentation, resource_name)
        property_type_list = self._get_type_list(property_type)
        if not property_type_list:
            return n
        for property_name in sorted(property_type_list):
            if property_name == "Tag" or property_name == "Tags":
                continue

            # prevent recursive properties
            if property_name == name:
                continue
            # This is a horrible hack to fix an indirect recursion issue in WAFv2
            # XXX - Need to implement a more durable solution to detect recursion
            if self.service_name == "wafv2" and property_name == "Statement":
                if self.statement_found:
                    continue
                else:
                    self.statement_found = True

            # Really a primitive type so stop recursing
            if property_name == "List":
                continue

            # Allow generic object for recursive use cases
            if property_name == "object":
                continue

            try:
                child = self._build_tree(
                    property_name,
                    self.properties[property_name],
                    "",
                    None,
                )
            except KeyError:
                print(
                    f"Could not find property '{property_name}' while generating service {self.service_name}",
                    file=sys.stderr,
                )
                print(
                    f"Available property keys:\n{sorted(self.properties.keys())}",
                    file=sys.stderr,
                )
                raise

            if child is not None:
                n.add_child(child)
        return n

    def _check_type(self, check_type: str, property: Property) -> bool:
        """Decode a properties type looking for a specific type."""

        if property.primitive_type:
            return property.primitive_type == check_type

        # If there's no Type defined, punt it for now...
        if not property.type:
            return False

        if property.type == "List":
            if property.item_type:
                return property.item_type == check_type
            else:
                return property.primitive_item_type == check_type

        return property.type == check_type

    def _walk_for_stub_type(self, check_type: str) -> bool:
        """
        Walk the resources/properties looking for a specific type.
        """
        for class_name, key, value in self.service.get_property_items():
            if value.type == check_type:
                return True
        return False

    def _walk_for_type(self, check_type: str) -> bool:
        """
        Walk the resources/properties looking for a specific type via _check_type()
        """
        for class_name, key, value in self.service.get_property_items():
            # Don't import if we will be overwriting with a validator
            if key in self.property_validators[class_name]:
                continue
            if self._check_type(check_type, value):
                return True
        return False

    def _walk_for_tags(self) -> bool:
        """
        Walk the resources/properties looking for tags
        """
        for class_name, key, value in self.service.get_property_items():
            # Don't import if we will be overwriting with a validator
            if key in self.property_validators[class_name]:
                continue
            if value.type == "Tags":
                return True
        return False

    def _get_type_list(
        self, property_type: Union[PropertyType, ResourceType]
    ) -> List[str]:
        """Return a list of non-primitive types used by this object."""
        type_list = []
        for key, value in property_type.properties.items():
            # Ignore primitive types or if there isn't a type field
            if value.primitive_type or not value.type:
                continue

            # Use the element (item type) for List or Maps
            if value.type == "List" or value.type == "Map":
                if value.item_type:
                    type_list.append(value.item_type)
            else:
                # Non-primitive (Property) name
                type_list.append(value.type)

        return sorted(type_list)

    def _generate_tree(self, t: Node, seen: Dict[str, bool]) -> List[str]:
        """Given a dependency tree of objects, generate it in DFS order."""
        if not t:
            return []
        code = []
        for c in t.children:
            code += self._generate_tree(c, seen)
        if t.name in seen:
            return []
        seen[t.name] = True

        if stub:
            code += self._generate_class_stub(t)
        else:
            class_validator = self.service.class_validators.get(t.name, None)
            property_validator = self.service.property_validators.get(t.name, None)
            code += self._generate_class(t, class_validator, property_validator)

        return code

    def _get_type(self, value: Property, stub=False):
        """Map AWS CloudFoundation types into Python types"""

        # For troposphere python code, use validation functions when appropriate
        map_type = {
            "Boolean": "boolean",
            "Double": "double",
            "Integer": "integer",
            "Json": "dict",
            "Long": "integer",
            "String": "str",
            "Timestamp": "str",
        }

        # For stub types, use the real Python3 types
        map_stub_type = {
            "Boolean": "bool",
            "Double": "float",
            "Integer": "int",
            "Json": "dict",
            "Long": "int",
            "String": "str",
            "Timestamp": "str",
        }

        if value.primitive_type:
            if stub:
                return map_stub_type.get(value.primitive_type, value.primitive_type)
            else:
                return map_type.get(value.primitive_type, value.primitive_type)

        if value.type is None:
            return "dict"

        if value.type == "List":
            if value.item_type:
                return "[%s]" % value.item_type
            else:
                if stub:
                    return "[%s]" % map_stub_type.get(
                        value.primitive_item_type, value.primitive_item_type
                    )
                else:
                    return "[%s]" % map_type.get(
                        value.primitive_item_type, value.primitive_item_type
                    )
        elif value.type == "Map":
            return "dict"
        else:
            # Non-primitive (Property) name
            return value.type

        import pprint

        pprint.pprint(value)
        raise ValueError("_get_type")

    def _generate_class(
        self, node: Node, class_validator, property_validator
    ) -> List[str]:
        class_name = node.name
        property_type = node.property_type
        resource_name = node.resource_name

        code = ["\n"]
        if resource_name:
            code.append(f"class {class_name}(AWSObject):")
            if property_type.documentation:
                code.append('    """')
                code.append(f"    `{class_name} <{property_type.documentation}>`__")
                code.append('    """')
                code.append("")
            code.append(f'    resource_type = "{resource_name}"')
            code.append("")
        else:
            code.append(f"class {class_name}(AWSProperty):")
            if property_type.documentation:
                code.append('    """')
                code.append(f"    `{class_name} <{property_type.documentation}>`__")
                code.append('    """')
                code.append("")

        # Output the props dict
        code.append("    props: PropsDictType = {")
        for key, value in sorted(property_type.properties.items()):
            if property_validator and key in property_validator:
                value_type = property_validator[key]
            elif value.type == "Tags":
                value_type = value.type
                if value.primitive_type == "Json":
                    value_type = "dict"
            else:
                value_type = self._get_type(value)

            # If the type is "object", we will ignore type errors.
            # "object" is usually patched in to prevent recursion
            ignore_type_error = "  # type: ignore" if value_type == "object" else ""

            required = value.required

            code.append(
                f'        "{key}": ({value_type}, {required}),{ignore_type_error}'
            )
        code.append("    }")
        if class_validator:
            code.append("")
            code.append("    def validate(self):")
            code.append(f"        {class_validator}(self)")

        return code

    def _generate_class_stub(self, node: Node) -> List[str]:
        class_name = node.name
        property_type = node.property_type
        resource_name = node.resource_name

        code = ["\n"]
        if resource_name:
            code.append(
                f"class {class_name}(AWSObject):",
            )
            code.append("    resource_type: str")
            code.append("")
            code.append("    def __init__(")
            code.append("        self,")
            code.append("        title,")
        else:
            code.append(
                f"class {class_name}(AWSProperty):",
            )
            code.append("")
            code.append("    def __init__(")
            code.append("        self,")

        for key, value in sorted(property_type.properties.items()):
            if key == "Tags":
                value_type = "Tags"
            else:
                value_type = self._get_type(value, stub=True)

            if value_type.startswith("["):  # Means that args are a list
                code.append(f"        {key}:List{value_type}=...,")
            else:
                code.append(f"        {key}:{value_type}=...,")

        code.append("    ) -> None: ...")
        code.append("")

        for key, value in sorted(property_type.properties.items()):
            if key == "Tags":
                value_type = "Tags"
            else:
                value_type = self._get_type(value, stub=True)

            if value_type.startswith("["):  # Means that args are a list
                code.append(f"    {key}: List{value_type}")
            else:
                code.append(f"    {key}: {value_type}")

        return code


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--stub", action="store_true", default=False)
    parser.add_argument("--directory", "-d", action="store")
    parser.add_argument("--filelist", action="store")
    parser.add_argument("--missing", "-m", action="store_true", default=False)
    parser.add_argument(
        "--spec", action="store", default="CloudFormationResourceSpecification.json"
    )
    parser.add_argument("service_names", nargs="*")
    parser.add_argument("--verbose", "-v", action="store_true", default=False)
    args = parser.parse_args()

    stub = args.stub
    verbose = args.verbose

    extension = ".py"
    if stub:
        extension = "pyi"

    if args.filelist and not args.directory:
        print("Must use -d option with the --filelist option")
        sys.exit(1)
    elif args.filelist:
        with open(args.filelist) as f:
            service_names = f.read().splitlines()
    else:
        if not args.service_names:
            print("No service names specified")
            sys.exit(1)
        service_names = [name.lower() for name in args.service_names]

    if args.verbose:
        print(f"Parsing resource specification file: {args.spec}", file=sys.stderr)
    r = ResourceSpec(args.spec).parse(limit_warnings=service_names)

    if args.missing:
        missing_resources = list(set(r.services.keys()).difference(set(service_names)))
        missing_resources.sort()
        print(f"Missing resources: {missing_resources}")
        sys.exit(0)

    for service_name in service_names:
        filename_base = service_to_filename(service_name)
        filename = f"{args.directory}/{filename_base}{extension}"

        if args.verbose:
            print(
                f"Generating service: {service_name} filename: {filename}",
                file=sys.stderr,
            )
        code = CodeGenerator(service_name, r.services[service_name]).generate()
        if args.directory:
            with open(filename, "w+") as f:
                print(code, file=f)
        else:
            print(code)
